<!DOCTYPE html>
<meta charset="utf-8">

<head>
    <title>D3 Curve Explorer</title>
</head>

<style>
    body {
        font-family: "Helvetica Neue", Helvetica, sans-serif;
        font-size: 12px;
        user-select: none;
    }

    svg {
        box-shadow: 0px 0px 40px -5px rgba(0, 0, 0, 0.3);
        border-radius: 5px;
        margin: 20px;
    }

    svg path {
        fill: none;
    }

    svg circle {
        fill: white;
        stroke: #aaa;
        cursor: move;
    }

    svg .points-menu g {
        opacity: 0.2;
    }

    svg .points-menu g.active {
        cursor: pointer;
        opacity: 1;
    }

    svg .points-menu line {
        stroke: #777;
        stroke-width: 3;
    }

    svg .points-menu rect {
        fill: white;
    }

    .sidebar {
        display: inline-block;
        position: relative;
        vertical-align: top;
        margin-top: 20px;
        width: 530px;
        color: #444;
    }

    .sidebar .header {
        font-size: 14px;
        font-weight: bold;
        text-align: right;
        color: #aaa;
        width: 100%;
    }

    .menu .item {
        padding: 5px;
        border: 1px solid #ddd;
        margin: 4px 2px;
        float: left;
        cursor: pointer;
        border-radius: 8px;
        width: 160px;
        text-align: center;
    }

    .sidebar .info {
        float: left;
        margin-top: 20px;
        line-height: 1.5em;
    }
</style>

<body>
    <svg width="700" height="400">
        <g>
            <path></path>
            <g class="points-menu" transform="translate(660, 380)">
                <g class="remove-point">
                    <rect x="-6" y="-6" width="12" height="12"></rect>
                    <line x1="-6" x2="6"></line>
                </g>
                <g class="add-point" transform="translate(20,0)">
                    <rect x="-6" y="-6" width="12" height="12"></rect>
                    <line x1="-6" x2="6"></line>
                    <line y1="-6" y2="6"></line>
                </g>
            </g>
        </g>
    </svg>

    <div class="sidebar">
        <div class="header">D3 CURVE EXPLORER</div>
        <div class="menu"></div>
        <div class="info">
            <span class="default">The JavaScript library <a href="https://d3js.org">D3</a> provides a number of <a
                    href="https://github.com/d3/d3-shape#curves">curve types</a> to interpolate (or approximate) a set
                of points. Toggle each of the curve types using the buttons above. You can also add/remove/drag the
                points to change the shape of the curve.</span>
            <span class="text"></span>
        </div>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.2/d3.min.js"></script>
    <script>
        var curveTypes = [
            { name: 'curveLinear', curve: d3.curveLinear, active: true, lineString: '', clear: false, info: 'Interpolates the points using linear segments.' },
            { name: 'curveBasis', curve: d3.curveBasis, active: true, lineString: '', clear: true, info: 'Interpolates the start and end points and approximates the inner points using a B-spline.' },
            { name: 'curveBasisClosed', curve: d3.curveBasisClosed, active: false, lineString: '', clear: false, info: 'Uses a closed B-Spline to approximate the points.' },
            { name: 'curveBundle (ß=0)', curve: d3.curveBundle.beta(0), active: false, lineString: '', clear: true, info: 'Same as curveBasis with the addition of a paramter ß which determines how close to a straight line the curve is. If ß=0 the curve is straight.' },
            { name: 'curveBundle (ß=0.5)', curve: d3.curveBundle.beta(0.5), active: false, lineString: '', clear: false, info: 'Same as curveBasis with the addition of a paramter ß which determines how close to a straight line the curve is.' },
            { name: 'curveBundle (ß=1)', curve: d3.curveBundle.beta(1), active: false, lineString: '', clear: false, info: 'Same as curveBasis with the addition of a paramter ß which determines how close to a straight line the curve is. If ß=1 the curve is the same as curveBasis.' },
            { name: 'curveCardinal (tension=0)', curve: d3.curveCardinal.tension(0), active: false, lineString: '', clear: true, info: "Interpolates the points using a cubic B-spline. A tension parameter determines how 'taut' the curve is. As tension approaches 1 the segments become linear." },
            { name: 'curveCardinal (tension=0.5)', curve: d3.curveCardinal.tension(0.5), active: false, lineString: '', clear: false, info: "Interpolates the points using a cubic B-spline. A tension parameter determines how 'taut' the curve is. As tension approaches 1 the segments become linear." },
            { name: 'curveCardinal (tension=1)', curve: d3.curveCardinal.tension(1), active: false, lineString: '', clear: false, info: "Interpolates the points using a cubic B-spline. A tension parameter determines how 'taut' the curve is. As tension approaches 1 the segments become linear." },
            { name: 'curveCatmullRom (α=0)', curve: d3.curveCatmullRom.alpha(0), active: false, lineString: '', clear: true, info: 'Similar to curveCardinal (tension=0) but with a parameter α that determines the parameterisation used to interpolate the points. If α=0 the parameterisation is uniform.' },
            { name: 'curveCatmullRom (α=0.5)', curve: d3.curveCatmullRom.alpha(0.5), active: false, lineString: '', clear: false, info: 'Similar to curveCardinal (tension=0) but with a parameter α that determines the parameterisation used to interpolate the points. If α=0.5 the parameterisation is centripetal and self intersecting loops are avoided.' },
            { name: 'curveCatmullRom (α=1)', curve: d3.curveCatmullRom.alpha(1), active: false, lineString: '', clear: false, info: 'Similar to curveCardinal (tension=0) but with a parameter α that determines the parameterisation used to interpolate the points. If α=1 the parameterisation is chordal.' },
            { name: 'curveMonotoneX', curve: d3.curveMonotoneX, active: false, lineString: '', clear: true, info: 'Interpolates the points with a cubic spline which are monotonic (i.e. always increasing or always decreasing) in y.' },
            { name: 'curveMonotoneY', curve: d3.curveMonotoneY, active: false, lineString: '', clear: false, info: 'Interpolates the points with a cubic spline which are monotonic (i.e. always increasing or always decreasing) in x.' },
            { name: 'curveNatural', curve: d3.curveNatural, active: false, lineString: '', clear: true, info: 'Interpolates the points with a cubic spline with zero 2nd derivatives at the endpoints.' },
            { name: 'curveStep', curve: d3.curveStep, active: false, lineString: '', clear: true, info: 'Interpolates the points with alternating horizontal and vertical linear segments. The vertical segments lie midway between points.' },
            { name: 'curveStepAfter', curve: d3.curveStepAfter, active: false, lineString: '', clear: false, info: 'Interpolates the points with alternating horizontal and vertical linear segments. The y value changes after the x value.' },
            { name: 'curveStepBefore', curve: d3.curveStepBefore, active: false, lineString: '', clear: false, info: 'Interpolates the points with alternating horizontal and vertical linear segments. The y value changes before the x value.' }
        ];

        var lineGenerator = d3.line();

        var categoryScale = d3.scaleOrdinal(d3.schemeCategory10);
        function colorScale(d) { return d === 0 ? '#777' : categoryScale(d); }

        var points = [[50, 330], [75, 200], [280, 75], [300, 75], [475, 300], [600, 200]];
        var numActivePoints = points.length;

        var drag = d3.drag()
            .on('drag', function (d, i) {
                points[i][0] = d3.event.x;
                points[i][1] = d3.event.y;
                update();
            });

        function updateInfo(info) {
            d3.select('.info .default').style('display', info ? 'none' : 'inline');
            d3.select('.info .text').text(info);
        }

        function updateMenu() {
            var u = d3.select('.menu')
                .selectAll('div.item')
                .data(curveTypes);

            u.enter()
                .append('div')
                .classed('item', true)
                .style('clear', function (d) { return d.clear ? 'left' : 'none'; })
                .text(function (d) { return d.name; })
                .on('click', function (d) {
                    d.active = !d.active;
                    update();
                })
                .on('mouseover', function (d) { updateInfo(d.info); })
                .on('mouseout', function () { updateInfo(''); })
                .merge(u)
                .style('background-color', function (d, i) { return d.active ? colorScale(i) : '#fff'; })
                .style('color', function (d, i) { return d.active ? 'white' : '#444'; });
        }

        function updatePointsMenu() {
            d3.select('.remove-point')
                .classed('active', numActivePoints > 2)
                .on('click', function () {
                    if (numActivePoints <= 2) return;
                    numActivePoints--;
                    update();
                });

            d3.select('.add-point')
                .classed('active', numActivePoints < points.length)
                .on('click', function () {
                    if (numActivePoints >= points.length) return;
                    numActivePoints++;
                    update();
                });
        }

        function updateLines() {
            curveTypes.forEach(function (d) {
                if (!d.active) return;
                lineGenerator.curve(d.curve);
                d.lineString = lineGenerator(points.slice(0, numActivePoints));
            });

            var u = d3.select('svg g')
                .selectAll('path')
                .data(curveTypes);

            u.enter()
                .append('path')
                .merge(u)
                .style('stroke', function (d, i) { return colorScale(i); })
                .attr('d', function (d) { return d.lineString; })
                .style('display', function (d) { return d.active ? 'inline' : 'none'; });
        }

        function updatePoints() {
            var u = d3.select('g')
                .selectAll('circle')
                .data(points.slice(0, numActivePoints));

            u.enter()
                .append('circle')
                .attr('r', 4)
                .call(drag)
                .merge(u)
                .attr('cx', function (d) { return d[0]; })
                .attr('cy', function (d) { return d[1]; });

            u.exit().remove();
        }

        function update() {
            updateMenu();
            updatePointsMenu();
            updateLines();
            updatePoints();
        }

        update();
    </script>
</body>

</html>